package com.mybatis.boost.mapper.model;

import com.mybatis.boost.domain.sort.Direction;
import com.mybatis.boost.domain.sort.Order;
import com.mybatis.boost.domain.sort.Sort;
import lombok.Data;
import lombok.Getter;
import org.springframework.jdbc.BadSqlGrammarException;
import tk.mybatis.mapper.entity.EntityColumn;
import tk.mybatis.mapper.entity.Example;
import tk.mybatis.mapper.entity.Example.Criteria;
import tk.mybatis.mapper.entity.IDynamicTableName;

import javax.validation.constraints.Min;
import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Stream;

import static com.google.common.base.Joiner.on;
import static com.google.common.collect.Lists.newArrayList;
import static com.mybatis.boost.mapper.util.sql.SqlUtil.removeDelimiter;
import static com.mybatis.boost.domain.sort.Order.DEFAULT_DIRECTION;
import static com.mybatis.boost.tool.StringUtil.trimAndToLowerCase;
import static java.util.stream.Collectors.toList;
import static tk.mybatis.mapper.mapperhelper.EntityHelper.getColumns;

/**
 * 单表WHERE条件
 *
 * @author jearton
 * @since 2016/12/9
 */
@SuppressWarnings("unused")
public class Condition implements IDynamicTableName {

    @Getter
    private final Example example;

    @Getter
    private final List<Order> orders;

    @Getter
    private Limit limit;

    private Condition() {
        this.example = null;
        this.orders = newArrayList();
    }

    private Condition(Class<?> entityClass) {
        this.example = new Example(entityClass);
        this.orders = newArrayList();
    }

    public static Condition from(Class<?> entityClass) {
        return new Condition(entityClass);
    }

    public static Condition empty() {
        return new Condition();
    }

    public void checkForEmpty() {
        if (this.example == null) {
            throw new IllegalArgumentException("it is an empty condition");
        }
    }

    public Condition orderBy(Sort... sort) {
        if (sort == null || sort.length == 0) {
            return this;
        }
        Stream.of(sort).filter(Objects::nonNull).forEach(s -> s.forEach(this::orderBy));
        return this;
    }

    public Condition orderBy(Order... order) {
        if (order == null || order.length == 0) {
            return this;
        }
        Stream.of(order).forEach(o -> orderBy(o.getProperty(), o.getDirection()));
        return this;
    }

    public Condition orderBy(String... field) {
        return orderBy(DEFAULT_DIRECTION, field);
    }

    public Condition orderBy(Direction direction, String... field) {
        Stream.of(field).forEach(f -> orderBy(f, direction));
        return this;
    }

    /**
     * 按指定字段排序
     *
     * @param field     字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     * @param direction 排序方向
     */
    public Condition orderBy(String field, Direction direction) {
        this.orders.add(new Order(direction, _toColumn(field)));
        return this;
    }

    /**
     * limit ${offset} ${size}
     */
    public Condition limit(int offset, int size) {
        this.limit = new Limit(offset, size);
        return this;
    }

    /**
     * distinct关键字
     */
    public Condition distinct(boolean distinct) {
        checkForEmpty();
        this.example.setDistinct(distinct);
        return this;
    }

    /**
     * 指定要查询的列，可以写属性名，也可以写表字段名
     */
    public Condition selectFields(String... fields) {
        checkForEmpty();
        String[] properties = Stream.of(fields).map(this::_toProperty).toArray(String[]::new);
        this.example.selectProperties(properties);
        return this;
    }

    /**
     * or条件
     */
    public Condition orCondition() {
        checkForEmpty();
        this.example.or();
        return this;
    }

    /**
     * WHERE ${field} = #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     * @param value 字段值
     */
    public Condition andEqualTo(String field, Object value) {
        _getLastCriteria().andEqualTo(_toProperty(field), value);
        return this;
    }

    public Condition eq(String field, Object value) {
        return andEqualTo(field, value);
    }

    public Condition isTrue(String field) {
        return eq(field, "true");
    }

    public Condition isFalse(String field) {
        return eq(field, "false");
    }

    /**
     * 将此对象的不为空的字段参数作为相等查询条件
     *
     * @param bean 参数对象
     */
    public Condition andEqualTo(Object bean) {
        _getLastCriteria().andEqualTo(bean);
        return this;
    }

    public Condition eq(Object bean) {
        return andEqualTo(bean);
    }

    /**
     * 聚合函数条件
     * <p>
     * 注意：此时左边条件中的字段名必须是数据库列名
     *
     * @param aggregation 例如 "length(country_name)="
     * @param value       例如 5
     */
    public Condition andCondition(String aggregation, Object value) {
        _getLastCriteria().andCondition(aggregation, value);
        return this;
    }

    public Condition condition(String aggregation, Object value) {
        return andCondition(aggregation, value);
    }

    /**
     * WHERE ${field} IS NULL
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andIsNull(String field) {
        _getLastCriteria().andIsNull(_toProperty(field));
        return this;
    }

    public Condition isNull(String field) {
        return andIsNotNull(field);
    }

    /**
     * WHERE ${field} IS NOT NULL
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andIsNotNull(String field) {
        _getLastCriteria().andIsNotNull(_toProperty(field));
        return this;
    }

    public Condition isNotNull(String field) {
        return andIsNotNull(field);
    }

    /**
     * WHERE ${field} != #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andNotEqualTo(String field, Object value) {
        _getLastCriteria().andNotEqualTo(_toProperty(field), value);
        return this;
    }

    public Condition ne(String field, Object value) {
        return andNotEqualTo(field, value);
    }

    /**
     * WHERE ${field} > #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andGreaterThan(String field, Object value) {
        _getLastCriteria().andGreaterThan(_toProperty(field), value);
        return this;
    }

    public Condition gt(String field, Object value) {
        return andGreaterThan(field, value);
    }

    /**
     * WHERE ${field} >= #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andGreaterThanOrEqualTo(String field, Object value) {
        _getLastCriteria().andGreaterThanOrEqualTo(_toProperty(field), value);
        return this;
    }

    public Condition ge(String field, Object value) {
        return andGreaterThanOrEqualTo(field, value);
    }

    /**
     * WHERE ${field} < #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andLessThan(String field, Object value) {
        _getLastCriteria().andLessThan(_toProperty(field), value);
        return this;
    }

    public Condition lt(String field, Object value) {
        return andGreaterThanOrEqualTo(field, value);
    }

    /**
     * WHERE ${field} <= #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andLessThanOrEqualTo(String field, Object value) {
        _getLastCriteria().andLessThanOrEqualTo(_toProperty(field), value);
        return this;
    }

    public Condition le(String field, Object value) {
        return andLessThanOrEqualTo(field, value);
    }

    /**
     * WHERE ${field} IN #{values}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andIn(String field, Collection<?> values) {
        if (values.size() == 0) {
            _getLastCriteria().andCondition(Boolean.FALSE.toString());
        } else {
            _getLastCriteria().andIn(_toProperty(field), values);
        }
        return this;
    }

    public Condition in(String field, Collection<?> values) {
        return andIn(field, values);
    }

    /**
     * WHERE ${field} NOT IN #{values}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andNotIn(String field, Collection<?> values) {
        if (values.size() == 0) {
            _getLastCriteria().andCondition(Boolean.FALSE.toString());
        } else {
            _getLastCriteria().andNotIn(_toProperty(field), values);
        }
        return this;
    }

    public Condition notIn(String field, Collection<?> values) {
        return andNotIn(field, values);
    }

    /**
     * WHERE ${field} BETWEEN #{value1} AND #{value2}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andBetween(String field, Object value1, Object value2) {
        _getLastCriteria().andBetween(_toProperty(field), value1, value2);
        return this;
    }

    public Condition between(String field, Object value1, Object value2) {
        return andBetween(field, value1, value2);
    }

    /**
     * WHERE ${field} NOT BETWEEN #{value1} AND #{value2}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andNotBetween(String field, Object value1, Object value2) {
        _getLastCriteria().andNotBetween(_toProperty(field), value1, value2);
        return this;
    }

    public Condition notBetween(String field, Object value1, Object value2) {
        return andNotBetween(field, value1, value2);
    }

    /**
     * WHERE ${field} LIKE #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andLike(String field, String value) {
        _getLastCriteria().andLike(_toProperty(field), value);
        return this;
    }

    public Condition like(String field, String value) {
        return andLike(field, value);
    }

    /**
     * WHERE ${field} NOT LIKE #{value}
     *
     * @param field 字段名（实体属性名或数据库列名皆可，不区分大小写），例：orderNo，orderno，order_no，`order_no`，ORDER_NO
     */
    public Condition andNotLike(String field, String value) {
        _getLastCriteria().andNotLike(_toProperty(field), value);
        return this;
    }

    public Condition notLike(String field, String value) {
        return andNotLike(field, value);
    }

    public Condition tableName(String tableName) {
        checkForEmpty();
        this.example.setTableName(tableName);
        return this;
    }

    private Criteria _getLastCriteria() {
        checkForEmpty();
        int size = this.example.getOredCriteria().size();
        if (size == 0) {
            return this.example.createCriteria();
        } else {
            return this.example.getOredCriteria().get(size - 1);
        }
    }

    public String toInfo() {
        StringBuilder condition = new StringBuilder();
        if (example == null) {
            return "empty condition.";
        }
        condition.append("class = ").append(this.example.getEntityClass()).append(", condition = ");
        List<String> orCondition = this.example.getOredCriteria().stream().map(criteria -> {
            List<String> andCondition = criteria.getCriteria().stream().map(criterion -> {
                if (criterion.isNoValue()) {
                    return criterion.getCondition();
                } else if (criterion.isBetweenValue()) {
                    return criterion.getCondition() + " " + criterion.getValue() + " and " + criterion.getSecondValue();
                } else {
                    return criterion.getCondition() + " " + criterion.getValue();
                }
            }).collect(toList());
            return "(" + on(" and ").join(andCondition) + ")";
        }).collect(toList());
        return condition.append(on(" or ").join(orCondition)).toString();
    }

    @Override
    public String getDynamicTableName() {
        return example == null ? "" : this.example.getDynamicTableName();
    }

    private String _toProperty(final String field) {
        return _matchColumnAndGet(field, EntityColumn::getProperty);
    }

    private String _toColumn(final String field) {
        return _matchColumnAndGet(field, EntityColumn::getColumn);
    }

    private String _matchColumnAndGet(final String field, Function<EntityColumn, String> mapper) {
        checkForEmpty();
        String lowerCaseField = removeDelimiter(trimAndToLowerCase(field));
        return getColumns(this.example.getEntityClass())
                .stream()
                .filter(column -> {
                    String property = column.getProperty().toLowerCase();
                    String columnName = removeDelimiter(column.getColumn()).toLowerCase();
                    return Objects.equals(property, lowerCaseField) || Objects.equals(columnName, lowerCaseField);
                })
                .map(mapper)
                .findFirst()
                .orElseThrow(() -> {
                    String reason = "Unknown property or column '" + field + "' in 'where clause'";
                    SQLException sqlException = new SQLSyntaxErrorException(reason, "42S22", 1054);
                    return new BadSqlGrammarException("Error querying database.  Cause: " + reason, "", sqlException);
                });
    }

    @Data
    public static class Limit {
        @Min(0)
        private final int offset;

        @Min(1)
        private final int size;

        Limit(int offset, int size) {
            if (offset < 0) {
                throw new IllegalArgumentException("offset must not be less than zero");
            }
            if (size < 1) {
                throw new IllegalArgumentException("size must not be less than one");
            }
            this.offset = offset;
            this.size = size;
        }
    }
}